#!/usr/bin/env python
#
# Redirect data from a TCP/IP connection to a serial port and vice versa.
#
# (C) 2002-2016 Chris Liechti <cliechti@gmx.net>
#
# SPDX-License-Identifier:    BSD-3-Clause

import sys
import socket
import serial
import serial.threaded
import time
import select
import threading

global clients
clients = []
debug = False


class SerialToNet(serial.threaded.Protocol):
    """serial->socket"""

    def __init__(self):
        self.socket = None

    def __call__(self):
        return self

    def data_received(self, data):
        if self.socket is not None:
            self.socket.sendall(data)
        global clients
        for item in clients:
            try:
                if debug:
                    print("ser->tcp",data)
                item.sendall(data)
            except:
                pass


def tcp_server(s, ser):
    global clients
    r_list = [s, ]
    while True:
        rl, wl, error = select.select(r_list, [], r_list)
        for fd in rl:
            if fd == s:
                conn, addr = fd.accept()
                # sys.stderr.write('Connected by {}\n'.format(addr))
                # try:
                #     conn.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 1)
                #     conn.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 1)
                #     conn.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 3)
                #     conn.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
                # except AttributeError:
                #     pass # XXX not available on windows
                # conn.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
                r_list.append(conn)
                clients.append(conn)
            else:
                try:
                    data = fd.recv(1024)
                    if not data:
                        r_list.remove(fd)
                        clients.remove(fd)
                        # sys.stderr.write('Disconnected by {}\n'.format(fd.getpeername()))
                        continue
                    ser.write(data)
                    while True:
                        try:
                            data = fd.recv(1024)
                            if not data:
                                break
                            ser.write(data)  # get a bunch of bytes and send them
                        except socket.error as msg:
                            # sys.stderr.write('ERROR: {}\n'.format(msg))
                            r_list.remove(fd)
                            clients.remove(fd)
                            fd.close()
                            break
                except ConnectionAbortedError:
                    r_list.remove(fd)
                    clients.remove(fd)
                    # sys.stderr.write('Disconnected by {}\n'.format(fd.getpeername()))
        for fd in error:
            # sys.stderr.write(" exception condition on ", s.getpeername())
            # stop listening for input on the connection
            r_list.remove(fd)
            clients.remove(fd)
            fd.close()


# 生成锁对象，全局唯一
lock = threading.Lock()


def tcp_client(ser, client_socket):
    sys.stderr.write('Connected by {}\n'.format(client_socket.getpeername()))
    try:
        client_socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 1)
        client_socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 1)
        client_socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 3)
        client_socket.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
    except AttributeError:
        pass  # XXX not available on windows
    client_socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
    try:
        clients.append(client_socket)
        # enter network <-> serial loop
        while True:
            try:
                data = client_socket.recv(1024)
                if not data:
                    break
                # 获取锁。未获取到会阻塞程序，直到获取到锁才会往下执行
                lock.acquire()
                if debug:
                    print("tcp->ser",data)
                ser.write(data)  # get a bunch of bytes and send them
                # 释放锁，归回倘，其他人可以拿去用了
                lock.release()
            except socket.error as msg:
                sys.stderr.write('ERROR: {}\n'.format(msg))
                # probably got disconnected
                break
    except KeyboardInterrupt:
        sys.stderr.write('KeyboardInterrupt\n')
    except socket.error as msg:
        sys.stderr.write('ERROR: {}\n'.format(msg))
    finally:
        clients.remove(client_socket)
        sys.stderr.write('Disconnected {}\n'.format(client_socket.getpeername()))
        client_socket.close()
    # 释放锁，归回倘，其他人可以拿去用了
    try:
        lock.release()
    except:
        pass


if __name__ == '__main__':  # noqa
    import argparse

    parser = argparse.ArgumentParser(
        description='Simple Serial to Network (TCP/IP) redirector.',
        epilog="""\
NOTE: no security measures are implemented. Anyone can remotely connect
to this service over the network.

Only one connection at once is supported. When the connection is terminated
it waits for the next connect.
""")

    parser.add_argument(
        'SERIALPORT',
        help="serial port name")

    parser.add_argument(
        'BAUDRATE',
        type=int,
        nargs='?',
        help='set baud rate, default: %(default)s',
        default=9600)

    parser.add_argument(
        '-q', '--quiet',
        action='store_true',
        help='suppress non error messages',
        default=False)

    parser.add_argument(
        '--develop',
        action='store_true',
        help='Development mode, prints Python internals on errors',
        default=False)

    group = parser.add_argument_group('serial port')

    group.add_argument(
        "--bytesize",
        choices=[5, 6, 7, 8],
        type=int,
        help="set bytesize, one of {5 6 7 8}, default: 8",
        default=8)

    group.add_argument(
        "--parity",
        choices=['N', 'E', 'O', 'S', 'M'],
        type=lambda c: c.upper(),
        help="set parity, one of {N E O S M}, default: N",
        default='N')

    group.add_argument(
        "--stopbits",
        choices=[1, 1.5, 2],
        type=float,
        help="set stopbits, one of {1 1.5 2}, default: 1",
        default=1)

    group.add_argument(
        '--rtscts',
        action='store_true',
        help='enable RTS/CTS flow control (default off)',
        default=False)

    group.add_argument(
        '--xonxoff',
        action='store_true',
        help='enable software flow control (default off)',
        default=False)

    group.add_argument(
        '--rts',
        type=int,
        help='set initial RTS line state (possible values: 0, 1)',
        default=None)

    group.add_argument(
        '--dtr',
        type=int,
        help='set initial DTR line state (possible values: 0, 1)',
        default=None)

    group = parser.add_argument_group('network settings')

    exclusive_group = group.add_mutually_exclusive_group()

    exclusive_group.add_argument(
        '-P', '--localport',
        type=int,
        help='local TCP port',
        default=7777)

    exclusive_group.add_argument(
        '-c', '--client',
        metavar='HOST:PORT',
        help='make the connection as a client, instead of running a server',
        default=False)

    args = parser.parse_args()

    # connect to serial port
    ser = serial.serial_for_url(args.SERIALPORT, do_not_open=True)
    ser.baudrate = args.BAUDRATE
    ser.bytesize = args.bytesize
    ser.parity = args.parity
    ser.stopbits = args.stopbits
    ser.rtscts = args.rtscts
    ser.xonxoff = args.xonxoff
    debug = args.develop
    
    if args.rts is not None:
        ser.rts = args.rts

    if args.dtr is not None:
        ser.dtr = args.dtr

    if not args.quiet:
        sys.stderr.write(
            '--- TCP/IP to Serial redirect on {p.name}  {p.baudrate},{p.bytesize},{p.parity},{p.stopbits} ---\n'
            '--- type Ctrl-C / BREAK to quit\n'.format(p=ser))

    try:
        ser.open()
    except serial.SerialException as e:
        sys.stderr.write('Could not open serial port {}: {}\n'.format(ser.name, e))
        sys.exit(1)

    ser_to_net = SerialToNet()
    serial_worker = serial.threaded.ReaderThread(ser, ser_to_net)
    serial_worker.start()

    if not args.client:
        srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        # srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        srv.bind(('', args.localport))
        sys.stderr.write('Listen on port:%d\n' % args.localport)
        srv.listen(5)
    try:
        while True:
            client_socket, addr = srv.accept()
            threading.Thread(target=tcp_client, args=(ser, client_socket)).start()
    except KeyboardInterrupt:
        srv.close()

    sys.stderr.write('\n--- exit ---\n')
    serial_worker.stop()
